-
Notifications
You must be signed in to change notification settings - Fork 2
Improve CQL Injection Query #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Will fix later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
… duplicate old test case
These are combined into the new CQL injection test file to avoid confusion; the few commits that precede this one "ports" the old test cases to the newer test file.
Added more test cases by moving over the test cases in the old cqlinjection.js (which was once moved to old/cqlinjection.js) and further split the test cases:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've completed a partial review, with some initial comments.
javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDS.qll
Outdated
Show resolved
Hide resolved
javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDS.qll
Outdated
Show resolved
Hide resolved
javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDS.qll
Show resolved
Hide resolved
This predicate is (1) not used anywhere, and (2) nothing but a simple alias to `API::Node.asSource/0`.
The original only covered `StringLiterals`, but `getStringValue` covers more cases ("constant folding").
…FromCdsLib/0` As stated in the new docstring of `CdsFacade`, the same `cds_facade` object can be accessed either via `@sap/cds` or `@sap/cds/lib`. Therefore, there is no distinction to be made between the two. Also, the `CdsDb` and the `GloballyAccessedCdsDbService` aimed to capture the same thing, so delete the latter in favor of the former (it's clearer and more concise).
…g to it This avoids duplicating the sink definition in `getQueryOfSink` and also in the `isSink` of the configuration.
Now, "@sap/cds/lib" is also a valid import path of a `CdsFacade` object.
* This step jumps from `id` in the property value expression to the enclosing object `{ id: "" + id }`. | ||
* This in conjunction with the above step 2 will make the taint tracker jump from `id` to the entire | ||
* INSERT clause. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain why we need this case? I would have expected an untrusted property value for id
could not cause an injection vulnerability in this query.
As a real world example, adding this step introduces a new result in cloud-cap-samples:
https://github.com/SAP-samples/cloud-cap-samples/blob/main/reviews/srv/reviews-service.js#L21-L25
That looks to me like a false positive, but perhaps I'm missing something!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, this step should only consider argument->returnvalue progagations where the argument is a string concatenation or originates from one.
I've addressed it in 689e00a. Now it no longer creates the FP alert on the reviews-service.js
.
|
||
ServiceInstance getRunner() { result = srv } | ||
|
||
SourceNode getContextObject() { | ||
result = this.getAnArgument().getALocalSource() and not result instanceof FunctionNode | ||
/* 1. An object node passed as the first argument to a call to `srv.tx`. */ | ||
result = txCall.getAnArgument().getALocalSource() and not result instanceof FunctionNode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says first argument, did you mean getArgument(0)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The srv.tx
method does not accept multiple context objects at once, and the callback function always comes after a context object argument, so technically this does what we want. However, it's still more accurate to just put down exactly what we want. 👍
javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDS.qll
Outdated
Show resolved
Hide resolved
* concatenation expression. e.g. | ||
* ``` javascript | ||
* cds.read("Entity1").where(`ID=${id}`); // Notice the surrounding parentheses! | ||
* cds.create("Entity1").entries({id: "" + id}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this case is covered by this class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm very glad you pointed this out, create
is indeed a hole in our coverage that needs to be modeled.
* A string concatenation expression included in a CQL shortcut method call. e.g. | ||
* ``` javascript | ||
* cds.read("Entity1").where(`ID=${id}`); // Notice the surrounding parentheses! | ||
* cds.create("Entity1").entries({id: "" + id}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the entries
cases in this qldoc are actually covered by the class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is related to our hole in the coverage for create
, thus will be addressed together.
|
||
this.on("send00144", async (req) => { | ||
const { id, amount } = req.data; | ||
cds.update("Entity1").set("col1 = col1" + amount).where`col1 = ${id}`; // FP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this considered an FP? Also, is set
permitted to have a string argument? The documentation says:
Specifies the data to update...
- As a single-expression tagged template string
- As an object with keys being element names of the target entity and values being simple values, query-by-example expressions, or CQN expressions:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ouch, that's labeled as FP by mistake. The where
clause is a tagged literal expression .where`col1 = ${id}`
which if it were only by itself would have been an FP (SAFE) case. But the .set("col = col1" + amount)
sitting there makes it a TP (UNSAFE) case.
|
||
this.on("send00174", async (req) => { | ||
const { id } = req.data; | ||
cds.delete("Entity1").where`ID = ${id}`; // FP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still an FP?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit misleading to mark a case as "FP" when what I really mean is this is a COMPLIANT
case and thus not something our query should not alert on.
Maybe I should use "SAFE" / "UNSAFE" instead of "FP" / (no label). I'll address them in a commit.
this.on("send00214", async (req) => { | ||
const { id } = req.data; | ||
const { Service1Entity } = this.entities; | ||
await SELECT.from(Service1Entity).where`ID=${id}`; // FP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still an FP?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is an FP (it's safe). Same strategy as this comment will be used and will be addressed together.
this.on("send00224", async (req) => { | ||
const { id } = req.data; | ||
const { Service1Entity } = this.entities; | ||
await INSERT.into(Service1Entity).entries`ID = ${id}`; // FP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a number of other FP markers in this file that should be reviewed - I haven't higlighted them all individually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is an FP (it's safe). Same strategy as this comment will be used and will be addressed together.
CallNode txCall; | ||
|
||
CdsTransaction() { | ||
txCall = srv.getAMemberCall("tx") and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This charpred either selects the tx call or the callback passed to the tx call - can you provide a quick example in the doc between the two? It's been very helpful where you've done that else where in the code.
or | ||
/* Imported from `cds.ql` */ | ||
exists(CdsFacade cds | | ||
cds.getMember("ql").getMember(name).getAValueReachableFromSource().asExpr() = this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this have to be a VarRef
?
/* Imported from `cds.ql` */ | ||
exists(CdsFacade cds | | ||
cds.getMember("ql").getMember(name).getAValueReachableFromSource().asExpr() = this | ||
exists(VarRef varRef | this = varRef | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exists
is currently superfluous. Did you mean to do something with different between varRef
and this
?
1. The second step should jump from a argument of a CQN object *only if the argument originates from a string concatentation*. Note that this new version identifies the end point using a successive application of `getAPredecessor`; it overapproximates and might accidentally include code that's not necessarily what we want. 2. The third is a specialization of the second step, and concerns itself only to the property writes to the object to be passed as an argument to the CQN query builder for INSERT and UPSERT.
Co-authored-by: Luke Cartey <[email protected]>
What this PR contributes
Improve CQL Injection Query.
CqlQueryRunnerCall
now expands and replacesTaintedCqlClause
, now coveringcds.run
,cds.db.run
,srv.run
, andtx.run
..run
and property readentities
:EntityEntry
is "absorbed" intoEntityReference
. Plus,EntityReference
now covers more examples, namely,cds
as a shortcut tocds.db
.Add robust test cases (This is the gist of this PR, please take a look for the behavioral summary description of what this PR aims to implement).
cds.run
and friends.await
-ing the query.this.run
and friends.Service2.run
and friends.cds.ql
.cds.parse.cql
.CQL
.Service2.tx( tx => tx.run(...) )
and friends.this.tx( tx => tx.run(...) )
and friends.cds.tx( tx => tx.run(...) )
and friends.cds.db.tx( tx => tx.run(...) )
and friends.Future works
cds.read(...).from(...)
, only thecds.read(...)
part is alerted on, where the entire chained method call is more desirable as a alert location.SensitiveExposure.ql
seems to be quite brittle, it needs a rewrite. This PR only updates the query's reference to old definitions that are no longer available.